const PASS = "PASS";
const FAIL = "FAIL";
const ERROR = "ERROR";

const styles = {
  [PASS]: { icon: "check", class: "success-response" },
  [FAIL]: { icon: "close", class: "cl-error-response" },
  [ERROR]: { icon: "close", class: "cl-error-response" },
  none: { icon: "", class: "" }
};

// TODO: probably have to use a more global state for `test`

export default function runTestScriptWithVariables(script, variables) {
  let pw = {
    _errors: [],
    _testReports: [],
    _report: "",
    expect(value) {
      try {
        return expect(value, this._testReports);
      } catch (e) {
        pw._testReports.push({ result: ERROR, message: e });
      }
    },
    test: (descriptor, func) => test(descriptor, func, pw._testReports)
    // globals that the script is allowed to have access to.
  };
  Object.assign(pw, variables);

  // run pre-request script within this function so that it has access to the pw object.
  new Function("pw", script)(pw);
  //
  const testReports = pw._testReports.map(item => {
    if (item.result) {
      item.styles = styles[item.result];
    } else {
      item.styles = styles.none;
    }
    return item;
  });
  return { report: pw._report, errors: pw._errors, testResults: testReports };
}

function test(descriptor, func, _testReports) {
  _testReports.push({ startBlock: descriptor });
  try {
    func();
  } catch (e) {
    _testReports.push({ result: ERROR, message: e });
  }
  _testReports.push({ endBlock: true });

  // TODO: Organize and generate text report of each {descriptor: true} section in testReports.
  // add checkmark or x depending on if each testReport is pass=true or pass=false
}

function expect(expectValue, _testReports) {
  return new Expectation(expectValue, null, _testReports);
}

class Expectation {
  constructor(expectValue, _not, _testReports) {
    this.expectValue = expectValue;
    this.not = _not || new Expectation(this.expectValue, true, _testReports);
    this._testReports = _testReports; // this values is used within Test.it, which wraps Expectation and passes _testReports value.
    this._satisfies = function(expectValue, targetValue) {
      // Used for testing if two values match the expectation, which could be === OR !==, depending on if not
      // was used. Expectation#_satisfies prevents the need to have an if(this.not) branch in every test method.
      // Signature is _satisfies([expectValue,] targetValue): if only one argument is given, it is assumed the targetValue, and expectValue is set to this.expectValue
      if (!targetValue) {
        targetValue = expectValue;
        expectValue = this.expectValue;
      }
      if (this.not === true) {
        // test the inverse. this.not is always truthly, but an Expectation that is inverted will always be strictly `true`
        return expectValue !== targetValue;
      } else {
        return expectValue === targetValue;
      }
    };
  }
  _fmtNot(message) {
    // given a string with "(not)" in it, replaces with "not" or "", depending if the expectation is expecting the positive or inverse (this._not)
    if (this.not === true) {
      return message.replace("(not)", "not ");
    } else {
      return message.replace("(not)", "");
    }
  }
  _fail(message) {
    this._testReports.push({ result: FAIL, message });
  }
  _pass(message) {
    this._testReports.push({ result: PASS });
  }
  // TEST METHODS DEFINED BELOW
  // these are the usual methods that would follow expect(...)
  toBe(value) {
    return this._satisfies(value)
      ? this._pass()
      : this._fail(
          this._fmtNot(`Expected ${this.expectValue} (not)to be ${value}`)
        );
  }
  toHaveProperty(value) {
    return this._satisfies(this.expectValue.hasOwnProperty(value), true)
      ? this._pass()
      : this._fail(
          this._fmtNot(
            `Expected object ${this.expectValue} to (not)have property ${value}`
          )
        );
  }
  toBeLevel2xx() {
    const code = parseInt(this.expectValue);
    if (Number.isNaN(code)) {
      return this._fail(
        `Expected 200-level status but could not parse value ${this.expectValue}`
      );
    }
    return this._satisfies(code >= 200 && code < 300)
      ? this._pass()
      : this._fail(
          this._fmtNot(
            `Expected ${this.expectValue} to (not)be 200-level status`
          )
        );
  }
  toBeLevel3xx() {
    const code = parseInt(this.expectValue);
    if (Number.isNaN(code)) {
      return this._fail(
        `Expected 300-level status but could not parse value ${this.expectValue}`
      );
    }
    return this._satisfies(code >= 300 && code < 400)
      ? this._pass()
      : this._fail(
          this._fmtNot(
            `Expected ${this.expectValue} to (not)be 300-level status`
          )
        );
  }
  toBeLevel4xx() {
    const code = parseInt(this.expectValue);
    if (Number.isNaN(code)) {
      return this._fail(
        `Expected 400-level status but could not parse value ${this.expectValue}`
      );
    }
    return this._satisfies(code >= 400 && code < 500)
      ? this._pass()
      : this._fail(
          this._fmtNot(
            `Expected ${this.expectValue} to (not)be 400-level status`
          )
        );
  }
  toBeLevel5xx() {
    const code = parseInt(this.expectValue);
    if (Number.isNaN(code)) {
      return this._fail(
        `Expected 500-level status but could not parse value ${this.expectValue}`
      );
    }
    return this._satisfies(code >= 500 && code < 600)
      ? this._pass()
      : this._fail(
          this._fmtNot(
            `Expected ${this.expectValue} to (not)be 500-level status`
          )
        );
  }
}
